using System;
using System.Text;
using NUnit.Framework;

namespace PaulRz.Util
{
    public sealed class ASCII7bitEncoding : Encoding
    {
        // both classes are state-less and that is why they are thread-safe
        private static Encoder encoder = new ASCII7bitEncoder();
        private static Decoder decoder = new ASCII7bitDecoder();

        public static ASCII7bitEncoding ASCII7bit = new ASCII7bitEncoding();
        
        ///<summary>
        ///When overridden in a derived class, calculates the number of bytes produced by encoding a set of characters from the specified character array.
        ///</summary>
        ///
        ///<returns>
        ///The number of bytes produced by encoding the specified characters.
        ///</returns>
        ///
        ///<param name="chars">The character array containing the set of characters to encode. </param>
        ///<param name="count">The number of characters to encode. </param>
        ///<param name="index">The index of the first character to encode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">index or count is less than zero.-or- index and count do not denote a valid range in chars. </exception>
        ///<exception cref="T:System.ArgumentNullException">chars is null. </exception><filterpriority>1</filterpriority>
        public override int GetByteCount(char[] chars, int index, int count)
        {
            return encoder.GetByteCount(chars, index, count, true);
        }

        ///<summary>
        ///When overridden in a derived class, encodes a set of characters from the specified character array into the specified byte array.
        ///</summary>
        ///
        ///<returns>
        ///The actual number of bytes written into bytes.
        ///</returns>
        ///
        ///<param name="chars">The character array containing the set of characters to encode. </param>
        ///<param name="charIndex">The index of the first character to encode. </param>
        ///<param name="charCount">The number of characters to encode. </param>
        ///<param name="byteIndex">The index at which to start writing the resulting sequence of bytes. </param>
        ///<param name="bytes">The byte array to contain the resulting sequence of bytes. </param>
        ///<exception cref="T:System.ArgumentNullException">chars is null.-or- bytes is null. </exception>
        ///<exception cref="T:System.ArgumentException">bytes does not have enough capacity from byteIndex to the end of the array to accommodate the resulting bytes. </exception>
        ///<exception cref="T:System.ArgumentOutOfRangeException">charIndex or charCount or byteIndex is less than zero.-or- charIndex and charCount do not denote a valid range in chars.-or- byteIndex is not a valid index in bytes. </exception><filterpriority>1</filterpriority>
        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
        {
            return encoder.GetBytes(chars, charIndex, charCount, bytes, byteIndex, true);
        }

        ///<summary>
        ///When overridden in a derived class, calculates the number of characters produced by decoding a sequence of bytes from the specified byte array.
        ///</summary>
        ///
        ///<returns>
        ///The number of characters produced by decoding the specified sequence of bytes.
        ///</returns>
        ///
        ///<param name="count">The number of bytes to decode. </param>
        ///<param name="index">The index of the first byte to decode. </param>
        ///<param name="bytes">The byte array containing the sequence of bytes to decode. </param>
        ///<exception cref="T:System.ArgumentNullException">bytes is null. </exception>
        ///<exception cref="T:System.ArgumentOutOfRangeException">index or count is less than zero.-or- index and count do not denote a valid range in bytes. </exception><filterpriority>1</filterpriority>
        public override int GetCharCount(byte[] bytes, int index, int count)
        {
            return decoder.GetCharCount(bytes, index, count, true);
        }

        ///<summary>
        ///When overridden in a derived class, decodes a sequence of bytes from the specified byte array into the specified character array.
        ///</summary>
        ///
        ///<returns>
        ///The actual number of characters written into chars.
        ///</returns>
        ///
        ///<param name="chars">The character array to contain the resulting set of characters. </param>
        ///<param name="byteCount">The number of bytes to decode. </param>
        ///<param name="byteIndex">The index of the first byte to decode. </param>
        ///<param name="charIndex">The index at which to start writing the resulting set of characters. </param>
        ///<param name="bytes">The byte array containing the sequence of bytes to decode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">byteIndex or byteCount or charIndex is less than zero.-or- byteindex and byteCount do not denote a valid range in bytes.-or- charIndex is not a valid index in chars. </exception>
        ///<exception cref="T:System.ArgumentNullException">bytes is null.-or- chars is null. </exception>
        ///<exception cref="T:System.ArgumentException">chars does not have enough capacity from charIndex to the end of the array to accommodate the resulting characters. </exception><filterpriority>1</filterpriority>
        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
        {
            return decoder.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
        }

        public byte[] GetCharsAsByte(byte[] str)
        {
            char[] chrs = GetChars(str);
            return Array.ConvertAll(chrs, converter);
        }

        private static Converter<char, byte> converter = new Converter<char, byte>(C2BConverter);
        
        private static byte C2BConverter(char input)
        {
            return (byte) input;
        }

        ///<summary>
        ///When overridden in a derived class, calculates the maximum number of bytes produced by encoding the specified number of characters.
        ///</summary>
        ///
        ///<returns>
        ///The maximum number of bytes produced by encoding the specified number of characters.
        ///</returns>
        ///
        ///<param name="charCount">The number of characters to encode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">charCount is less than zero. </exception><filterpriority>1</filterpriority>
        public override int GetMaxByteCount(int charCount)
        {
            return ASCII7bitEncoder.GetMaxBytes(charCount);
        }

        ///<summary>
        ///When overridden in a derived class, calculates the maximum number of characters produced by decoding the specified number of bytes.
        ///</summary>
        ///
        ///<returns>
        ///The maximum number of characters produced by decoding the specified number of bytes.
        ///</returns>
        ///
        ///<param name="byteCount">The number of bytes to decode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">byteCount is less than zero. </exception><filterpriority>1</filterpriority>
        public override int GetMaxCharCount(int byteCount)
        {
            return ASCII7bitDecoder.GetMaxChars(byteCount);
        }

        #region Tests
        
        [Test]
        public static void testEncDec()
        {
            string test = "testtest"; // 8 chars should be packed into 7 bytes
            Encoding ascii7bit = new ASCII7bitEncoding();
            byte[] bytes = ascii7bit.GetBytes(test);
            string ntest = ascii7bit.GetString(bytes);
            Console.WriteLine("Source : {0}", test);
            Console.WriteLine("Dest   : {0}", ntest);
            Console.WriteLine("Equivalence : "+test.CompareTo(ntest));
        }
        
        [Test]
        public static void testEnc()
        {
            byte[] testStr = new byte[] { 0xE8, 0x32, 0x9B, 0xFD, 0x46, 0x97, 0xD9, 0xEC, 0x37 };
            string str = "hellohello";
            byte[] bytes = (new ASCII7bitEncoding()).GetBytes(str);
            
            bool isEqual = true;
            if (testStr.Length == bytes.Length)
            {
                for(int i=0; i<bytes.Length; ++i)
                {
                    if (bytes[i]!=testStr[i])
                    {
                        isEqual = false;
                        break;
                    }
                }
            }
            else
            {
                isEqual = false;
                Console.WriteLine("Different sizes!");
            }
            
            Console.WriteLine("Equivalence : {0}", isEqual);
        }
        
        #endregion
    }
    
    internal sealed class ASCII7bitEncoder : Encoder
    {
        ///<summary>
        ///When overridden in a derived class, calculates the number of bytes produced by encoding a set of characters from the specified character array. A parameter indicates whether to clear the internal state of the encoder after the calculation.
        ///</summary>
        ///
        ///<returns>
        ///The number of bytes produced by encoding the specified characters and any characters in the internal buffer.
        ///</returns>
        ///
        ///<param name="chars">The character array containing the set of characters to encode. </param>
        ///<param name="count">The number of characters to encode. </param>
        ///<param name="flush">true to simulate clearing the internal state of the encoder after the calculation; otherwise, false. </param>
        ///<param name="index">The index of the first character to encode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">index or count is less than zero.-or- index and count do not denote a valid range in chars. </exception>
        ///<exception cref="T:System.ArgumentNullException">chars is null. </exception><filterpriority>2</filterpriority>
        public override int GetByteCount(char[] chars, int index, int count, bool flush)
        {
            if (chars == null)
                throw new ArgumentNullException("chars");

            if ((index < 0) || (chars.Length - 1 < index))
                throw new ArgumentOutOfRangeException("index", index, "Index is out of range");

            if ((count < 0) || (index + count > chars.Length))
                throw new ArgumentOutOfRangeException("count", count, "Count is out of range");

            // in the case it's the same
            return GetMaxBytes(count);
        }

        ///<summary>
        ///When overridden in a derived class, encodes a set of characters from the specified character array and any characters in the internal buffer into the specified byte array. A parameter indicates whether to clear the internal state of the encoder after the conversion.
        ///</summary>
        ///
        ///<returns>
        ///The actual number of bytes written into bytes.
        ///</returns>
        ///
        ///<param name="chars">The character array containing the set of characters to encode. </param>
        ///<param name="charIndex">The index of the first character to encode. </param>
        ///<param name="charCount">The number of characters to encode. </param>
        ///<param name="byteIndex">The index at which to start writing the resulting sequence of bytes. </param>
        ///<param name="flush">true to clear the internal state of the encoder after the conversion; otherwise, false. </param>
        ///<param name="bytes">The byte array to contain the resulting sequence of bytes. </param>
        ///<exception cref="T:System.ArgumentNullException">chars is null.-or- bytes is null. </exception>
        ///<exception cref="T:System.ArgumentException">bytes does not have enough capacity from byteIndex to the end of the array to accommodate the resulting bytes. </exception>
        ///<exception cref="T:System.ArgumentOutOfRangeException">charIndex or charCount or byteIndex is less than zero.-or- charIndex and charCount do not denote a valid range in chars.-or- byteIndex is not a valid index in bytes. </exception><filterpriority>2</filterpriority>
        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, bool flush)
        {
            if (chars == null)
                throw new ArgumentNullException("chars");
            if (bytes == null)
                throw new ArgumentNullException("bytes");
            
            if (charIndex < 0 || (charIndex >= chars.Length))
                throw new ArgumentOutOfRangeException("charIndex");
            if ((charIndex + charCount > chars.Length))
                throw new ArgumentOutOfRangeException("charIndex/charCount");
            if (byteIndex < 0 || (byteIndex >= bytes.Length))
                throw new ArgumentOutOfRangeException("byteIndex");

            int byteCount = GetByteCount(chars, charIndex, charCount, true);
            int curByte = byteIndex;
            int curBit = 0;

            byte[] asciiChars = Encoding.ASCII.GetBytes(chars, charIndex, charCount);
            byte[] maskBytes = new byte[] {0, 0};

            for(int j=byteIndex; j<byteIndex+byteCount; ++j)
            {
                bytes[j] = 0;
            }
            
            for (int i = 0; i < asciiChars.Length; ++i)
            {
                byte toAdd = asciiChars[i];
                
                if ((toAdd & 0x80)!=0)
                    throw new ArgumentOutOfRangeException("chars", toAdd, "Illegal character in input array");
                
                fillMask(toAdd, curBit, maskBytes);
                bytes[curByte] |= maskBytes[0];
                
                if (curByte < byteCount-1)
                    bytes[curByte + 1] |= maskBytes[1];
                
                curBit += 7;
                if (curBit>7)
                {
                    curByte++;
                    curBit -= 8;
                }
            }

            return byteCount;
        }
        
        private static void fillMask(byte toAdd, int curBit, byte[] mask)
        {
            unchecked
            {
                int masks = (ushort)(toAdd & 0x7F);
                // we have to shift to high byte caring about the position curBit
                masks = masks << curBit;
                mask[0] = (byte)masks;
                mask[1] = (byte)(masks >> 8);
            }
        }
        
        internal static int GetMaxBytes(int count)
        {
            int resCount = count * 7;
            int rem;
            resCount = Math.DivRem(resCount, 8, out rem);
            if (rem > 0)
                resCount++;

            return resCount;
        }
    }
    
    internal sealed class ASCII7bitDecoder : Decoder
    {
        ///<summary>
        ///When overridden in a derived class, calculates the number of characters produced by decoding a sequence of bytes from the specified byte array.
        ///</summary>
        ///
        ///<returns>
        ///The number of characters produced by decoding the specified sequence of bytes and any bytes in the internal buffer.
        ///</returns>
        ///
        ///<param name="count">The number of bytes to decode. </param>
        ///<param name="index">The index of the first byte to decode. </param>
        ///<param name="bytes">The byte array containing the sequence of bytes to decode. </param>
        ///<exception cref="T:System.ArgumentNullException">bytes is null. </exception>
        ///<exception cref="T:System.ArgumentOutOfRangeException">index or count is less than zero.-or- index and count do not denote a valid range in bytes. </exception><filterpriority>2</filterpriority>
        public override int GetCharCount(byte[] bytes, int index, int count)
        {
            if (bytes == null)
                throw new ArgumentNullException("bytes");

            if ((index < 0) || (bytes.Length - 1 < index))
                throw new ArgumentOutOfRangeException("index", index, "Index is out of range");

            if ((count < 0) || (index + count > bytes.Length))
                throw new ArgumentOutOfRangeException("count", count, "Count is out of range");

            return GetMaxChars(count);
        }

        ///<summary>
        ///When overridden in a derived class, decodes a sequence of bytes from the specified byte array and any bytes in the internal buffer into the specified character array.
        ///</summary>
        ///
        ///<returns>
        ///The actual number of characters written into chars.
        ///</returns>
        ///
        ///<param name="chars">The character array to contain the resulting set of characters. </param>
        ///<param name="byteCount">The number of bytes to decode. </param>
        ///<param name="byteIndex">The index of the first byte to decode. </param>
        ///<param name="charIndex">The index at which to start writing the resulting set of characters. </param>
        ///<param name="bytes">The byte array containing the sequence of bytes to decode. </param>
        ///<exception cref="T:System.ArgumentOutOfRangeException">byteIndex or byteCount or charIndex is less than zero.-or- byteindex and byteCount do not denote a valid range in bytes.-or- charIndex is not a valid index in chars. </exception>
        ///<exception cref="T:System.ArgumentNullException">bytes is null.-or- chars is null. </exception>
        ///<exception cref="T:System.ArgumentException">chars does not have enough capacity from charIndex to the end of the array to accommodate the resulting characters. </exception><filterpriority>2</filterpriority>
        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
        {
            if (chars == null)
                throw new ArgumentNullException("chars");
            if (bytes == null)
                throw new ArgumentNullException("bytes");
            if ((byteIndex < 0) || (byteIndex>bytes.Length) || (byteIndex + byteCount > bytes.Length))
                throw new ArgumentOutOfRangeException("byteIndex");
            if (charIndex < 0 || (charIndex >= chars.Length))
                throw new ArgumentOutOfRangeException("charIndex");
            
            int outSize = GetCharCount(bytes, byteIndex, byteCount);
            if (charIndex + outSize > chars.Length)
                throw new ArgumentException("chars");

            int curIndex = byteIndex;
            int curBit = 0;
            int maxIndex = bytes.Length-1;

            Encoding ascii = Encoding.ASCII;
            byte[] convArr = new byte[1];
            char[] strArr = new char[1];
            byte[] mask = new byte[2];
            
            for (int i = charIndex; i < charIndex + outSize; ++i)
            {
                mask[0] = bytes[curIndex];
                
                if (curIndex < maxIndex)
                    mask[1] = bytes[curIndex + 1];
                else
                    mask[1] = 0;
                
                convArr[0] = extract7bit(mask, curBit);
                ascii.GetChars(convArr, 0, 1, strArr, 0);
                chars[i] = strArr[0];

                curBit += 7;
                if (curBit > 7)
                {
                    curIndex++;
                    curBit -= 8;
                }
            }

            return outSize;
        }
        
        private static byte extract7bit(byte[] bytes, int startBit)
        {
            unchecked
            {
                int bits = bytes[0] + (((int)bytes[1]) << 8);
                return (byte)((bits >> startBit) & 0x7F);
            }
        }
        
        internal static int GetMaxChars(int count)
        {
            int resCount = count * 8;
            int rem;
            resCount = Math.DivRem(resCount, 7, out rem);

            return resCount;
        }
    }
}
